#!/usr/bin/env python
# encoding: utf-8
'''
@author: guihehans
@file: jenkins_api.py
@time: 2020/4/1 9:51
@function:

'''
import re
from concurrent.futures.thread import ThreadPoolExecutor
from queue import Queue

import jenkins
from time import sleep
# 使用同步方式编写异步功能
import time
import asyncio
import sys
from multiprocessing import Pool
import requests

from jenkins_tool.common import EnhanceUtills, const
from jenkins_tool.common import CrowdSSO
from jenkins_tool.common.CrowdSSO import CrowdClient
from jenkins_tool.log import Logger
from jenkins_tool.service import gittag_extract

module_name = "jenkins_api"
system_id = const.SYS_ID

TIME_TO_QUEUE = 30

global logger
logger = Logger.Logger(clevel=const.Logging_command_level, Flevel=const.Logging_file_level)


def connect_server(token: str, crowd_client: CrowdClient, env="pre"):
    """
    connect the jenkins server using crowd token. access the user credential stored in CrowdClient and access
    the jenkins server.

    :param token: a token from Crowd authentication
    :param crowd_client: a CrowdClient which stores the user's credential
    :param env: the jenkins server running environment. "pre" for env=pre, "prod" for env=prod
    :return: a jenkins server object if success or null if failed
    """
    try:
        # get the user credential from a
        username, password = crowd_client.get_login_info(token)
        # if the env is prod
        if env == "pre":
            jenkins_host = const.JENKINS_HOST
            port = const.JENKINS_PORT
        # if the env is prod
        if env == "prod":
            jenkins_host = const.PROD_JENKINS_HOST
            port = const.PROD_JENKINS_PORT
        jenkins_url = 'http://{}:{}/jenkins'.format(jenkins_host, port)
        server = jenkins.Jenkins(jenkins_url, username=username, password=password)
        logger.info(module_name, sys._getframe().f_code.co_name,
                    "User={} Connect to JENKINS_SERVER={} on ENV={} SUCCESS.".format(username, jenkins_url, env)
                    , system_id
                    )
        return server
    except Exception as e:
        logger.info(module_name, sys._getframe().f_code.co_name,
                    "User name or password not correct!"
                    , "Authentication Failed"
                    )
        return None


def get_sys_id_from_str(input_str: str) -> str:
    """
    return a sys_id str from string. the case is ignored
    e.g, input "ld075_bmgs_deply2" will return "ld075"

    :param input_str: the input string param.
    :return: sys_id:str
    """
    re_str = r'[lL][dD](\d)*'
    pattern = re.compile(re_str)
    # return a match
    list_str = pattern.search(input_str)
    # span() return the (start,end) index tuple for the match
    start, end = (list_str.span())
    # get the sys_id from release_version str.
    sys_id = input_str[start:end]
    return sys_id


def get_jobname_by_sysid_and_suffix(server, sysid: str, suffix: str = "prod"):
    """
    search the job_name in jenkins by sys_id and suffix.
    In the build phase, the job suffix is "prod";
    In the pre deploy phase, the job suffix is "pre_deply";
    In the prod deploy phase, the job suffix is "deply[\d]"

    :param server: jenkins.jenkins instance
    :param sysid: the sys id, e.g, "ld171"
    :param suffix: the suffix to identify job.
    :return: the job name or "" if not found
    """
    re_str = r'^{}(.)*_{}$'.format(sysid, suffix)
    # using jenkins api get_job_info_regex(re_str) to get job name
    try:
        job_info_list = server.get_job_info_regex(re_str)
        logger.info(module_name, sys._getframe().f_code.co_name,
                    "the job matched by sys_id: {} is: {}".format(sysid, job_info_list[0]['name'])
                    , system_id
                    )
        job = (job_info_list[0]['name'])
        return job
    except IndexError as e:
        logger.error(module_name, sys._getframe().f_code.co_name, EnhanceUtills.traceback_printer(), system_id)
        return ""


def build_job_by_param(server, job, qaVersion, task_id, opser, release_memo, release_exception):
    """
    build job by param and return next build number and queue item
    :param server: a jenkins server
    :param job: job name
    :param qaVersion:
    :param task_id:
    :param opser:
    :param release_memo:
    :param release_exception:
    :return:next_build_number:str, queue_item:int
    """
    param = {
        'qaVersion': qaVersion,
        'TASK_NO': task_id,
        'opser': opser,
        'release_memo': release_memo,
        'release_exception': release_exception
    }
    logger.info(module_name, sys._getframe().f_code.co_name,
                "the build job:{} with param:{} is building".format(job, param)
                , system_id
                )

    queue_item = server.build_job(job, param)
    # get next_build_number
    build_number = server.get_job_info(job)['nextBuildNumber']
    logger.info(module_name, sys._getframe().f_code.co_name,
                "the job:{} with param:{} build complete. the queue item is:{}, the next build number is :{}".format(
                    job,
                    param,
                    queue_item,
                    build_number)
                , system_id
                )
    return build_number, queue_item


def build_redmine_insert_job(server: jenkins.Jenkins, task_id: str):
    job = 'auto_release_redmine_insert'
    param = {
        'TASK_NO': task_id
    }
    logger.info(module_name, sys._getframe().f_code.co_name,
                "start building redmine insert job with task_id:{}".format(task_id)
                , system_id
                )
    # build job by job and param
    queue_item = server.build_job(job, param)
    # get next_build_number
    next_build_number = server.get_job_info(job)['nextBuildNumber']
    logger.info(module_name, sys._getframe().f_code.co_name,
                "complete building redmine insert job with task_id:{}".format(task_id)
                , system_id
                )
    return job, next_build_number, queue_item


def build_cmdb_insert_job(server: jenkins.Jenkins, pre_release_version: str, release_note: str,
                          release_exception: str):
    """
    build the auto_release_cmdb_insert job.
    :param server:
    :param preRelease_version:
    :param release_note:
    :param release_exception:
    :return:job_name:str, next_build_number:str, queue_item:int
    """
    job_name = 'auto_release_cmdb_insert'
    param = {
        'preRelease_version': pre_release_version,
        'release_note': release_note,
        'release_exception': release_exception
    }
    logger.info(module_name, sys._getframe().f_code.co_name,
                "start building cmdb insert job with preRelease_version:{}".format(pre_release_version)
                , system_id
                )
    # build job by job and param
    queue_item = server.build_job(job_name, param)
    # get next_build_number
    next_build_number = server.get_job_info(job_name)['nextBuildNumber']
    logger.info(module_name, sys._getframe().f_code.co_name,
                "complete building cmdb insert job with preRelease_version:{}".format(pre_release_version)
                , system_id
                )
    return job_name, next_build_number, queue_item


def build_deploy_job(server: jenkins.Jenkins, job_name: str, release_version: str):
    job = job_name
    param = {
        'GitVersion': release_version
    }
    logger.info(module_name, sys._getframe().f_code.co_name,
                "start building deploy job:{} with release_version:{}".format(job_name, release_version)
                , system_id
                )
    # build job by job and param
    queue_item = server.build_job(job, param)
    # get next_build_number
    next_build_number = server.get_job_info(job)['nextBuildNumber']
    logger.info(module_name, sys._getframe().f_code.co_name,
                "complete building deploy job with release_version:{}".format(release_version)
                , system_id
                )
    return job, next_build_number, queue_item


def update_deploy_record_job(server: jenkins.Jenkins, release_version: str):
    """
    the detail execution function to build add_deploy_record_job
    
    :param server: 
    :param release_version: 
    :return: 
    """
    job = "update_deploy_count_to_cmdb_prod"
    param = {
        'GitVersion': release_version
    }
    logger.info(module_name, sys._getframe().f_code.co_name,
                "start building add_deploy_record_job with release_version:{}".format(release_version)
                , system_id
                )
    # build job by job and param
    queue_item = server.build_job(job, param)
    # get next_build_number
    next_build_number = server.get_job_info(job)['nextBuildNumber']
    logger.info(module_name, sys._getframe().f_code.co_name,
                "complete building add_deploy_record_job with release_version:{}".format(release_version)
                , system_id
                )
    return job, next_build_number, queue_item


def get_build_exec_state(server, job: str, build_number: int):
    """
    get the build info according to job and build number.
    invoke jenkins.get_build_info(job,build_number)

    :param server: a jenkins.jenkins instance
    :param job: job_name. str
    :param build_number: a build number. int
    :return:
    """
    # get the build info.
    build_info = server.get_build_info(job, int(build_number))
    return build_info


def build_job_from_info(server, task_id: int, sys_id: str, git_tag: str, opser: str, release_memo: str,
                        release_exception: str) -> (str, str, int):
    """
    build job from given param. return the job name and system given next build number
    with the build number, the job execution info can be queried by Func:get_build_exec_state(server, job, build_number)

    :param server: jenkins_server
    :param task_id: a task id. e.g. 28961
    :param sys_id: a system id. e.g. ld403
    :param git_tag: a test verified git tag
    :param opser: the opser name
    :param release_memo: the release note info
    :param release_exception: the release exception info, multi build needed

    :return: the jobname and build_number and queue item
    """
    try:
        jenkins_server = server
        job_name = get_jobname_by_sysid_and_suffix(jenkins_server, sys_id, suffix="prod")
        build_number, queue_item = build_job_by_param(jenkins_server, job_name, git_tag, task_id, opser, release_memo,
                                                      release_exception)
        return job_name, build_number, queue_item
    except Exception as e:
        logger.info(module_name, sys._getframe().f_code.co_name,
                    "Build job with git_tag:{} failed. Check jenkins to see more "
                    "details.".format(git_tag),
                    system_id)
        logger.error(module_name, sys._getframe().f_code.co_name, EnhanceUtills.traceback_printer(),
                     system_id)


def query_build_execution(server: jenkins.Jenkins, job_name: str, queue_item: int, build_number: int):
    """
    Restful api for query a job's queue and build execution status.
    :param server: a jenkins.Jenkins instance represents a jenkins server.
    :param queue_item: the queue item returned from build job process.
    :param job_name: the job name for build.
    :param build_number: a build number for job build.
    :return: a dict instance. "Status" key can be {"Waiting","Building","Finished"}.
    the "Response" will have the information from query.
    """
    # first try use queue_item as query method to ensure the job is executed on jenkins.
    # but the queue_item is only kept for 5 minutes. if expired the query rely on build_number.
    try:
        queue_info = server.get_queue_item(queue_item)
        if queue_info.get("why") is not None:
            return {"job_name": job_name, "Status": "Waiting", "Response": "Waiting for queue."}
        if queue_info.get("executable") is not None:
            queried_build_number = queue_info.get("executable").get("number")
            resp = query_build_by_build_number(server, job_name, queried_build_number)
            return resp
    # if queue item expired, use build number to query
    except jenkins.JenkinsException:
        print("queue item {} not exist, try fetch by build number".format(queue_item))
        resp = query_build_by_build_number(server, job_name, build_number)
        return resp


def query_build_by_build_number(server: jenkins.Jenkins, job_name: str, build_number: int):
    """
    sub function to query build info by build number.
    :param server: a jenkins.Jenkins instance represents a jenkins server.
    :param job_name: the job name for build.
    :param build_number: a build number for job build.

    :return: a dict instance contains job_name,build_number,status and response. "Status" key can be {"Building","Finished"}.
    the "Response" will have the information from query.
    """
    build_info = get_build_exec_state(server, job_name, build_number)
    building = build_info['building']
    result = build_info['result']
    # while the job is still building, return the estimated progress
    if building:
        # get the progress percentage
        progress = ((time.time() * 1000 - (build_info['timestamp'])) / build_info['estimatedDuration']) * 100
        # if progress > 100, set it to 99.99
        if progress > 100:
            progress = 99.99
        logger.info(module_name, sys._getframe().f_code.co_name,
                    "job_name:{} ,build_number:{} is building, estimated progress is: {:2.2f}%".format(job_name,
                                                                                                       build_number,
                                                                                                       progress),
                    system_id)
        return {"job_name": job_name, "build_number": build_number, "Status": "Building",
                "Response": "{:2.2f}".format(progress)}
    # when building finished, return result
    return {"job_name": job_name, "build_number": build_number, "Status": "Finished",
            "Response": result}


"""
build phase related function end.
deploy phase related function start.
"""


def build_pre_deploy_job_from_info(server, release_version: str) -> (str, str, int):
    """
    build deploy job by given param on env=pre.

    :param server: jenkins_server
    :param release_version: a release version info. e.g. 'ld171_fims-gateway_No.29339_20200707.1_pre'
    :return: the jobname, build_number, queue item
    """
    try:
        jenkins_server = server
        sys_id = get_sys_id_from_str(release_version)
        job_name = get_jobname_by_sysid_and_suffix(jenkins_server, sys_id, suffix="pre_deply")
        job, next_build_number, queue_item = build_deploy_job(jenkins_server, job_name, release_version)
        return job, next_build_number, queue_item
    except Exception as e:
        logger.error(module_name, sys._getframe().f_code.co_name, EnhanceUtills.traceback_printer(),
                     system_id)


def get_prod_deploy_job_name_list_by_sys_id(server, sys_id: str, suffix="deply"):
    """
    get the job_name list by given sys_id on env='prod'

    :param server: a jenkins.jenkins instance
    :param sys_id: the system id. e.g, ld107
    :param suffix: the suffix to match the job name in jenkins
    :return: job_name_list as a list.return [] if not found.
    """
    re_str = r'^{}(.)*_{}[\d]$'.format(sys_id, suffix)
    # using jenkins api get_job_info_regex(re_str) to get job name
    job_name_list = []
    try:
        job_info_list = server.get_job_info_regex(re_str)
        for i, job_info in enumerate(job_info_list):
            if job_info.get("buildable") is True:
                job_name_list.append(job_info['name'])
        logger.info(module_name, sys._getframe().f_code.co_name,
                    "the job list matched by sys_id: {} is: {}".format(sys_id, job_name_list)
                    , system_id
                    )
    except IndexError as e:
        logger.error(module_name, sys._getframe().f_code.co_name, EnhanceUtills.traceback_printer(), system_id)
    finally:
        return job_name_list


async def async_get_prod_deploy_job_name_list_by_sys_id(server, sys_id: str, suffix="deply"):
    """
    get the job_name list by given sys_id on env='prod' in async mode.
    Use a ThreadPoolExecutor, create a new thread to execute the job

    :param server: a jenkins.jenkins instance
    :param sys_id: the system id. e.g, ld107
    :param suffix: the suffix to match the job name in jenkins
    :return: job_name_list as a list.return [] if not found.
    """

    re_str = r'^{}(.)*_{}[\d]$'.format(sys_id, suffix)
    # using jenkins api get_job_info_regex(re_str) to get job name
    job_name_list = []
    try:
        executor = ThreadPoolExecutor(1)
        loop = asyncio.get_event_loop()
        job_info_list = await loop.run_in_executor(executor, server.get_job_info_regex, re_str)
        for i, job_info in enumerate(job_info_list):
            if job_info.get("buildable") is True:
                job_name_list.append(job_info['name'])
        logger.info(module_name, sys._getframe().f_code.co_name,
                    "the job list matched by sys_id: {} is: {}".format(sys_id, job_name_list)
                    , system_id
                    )
    except IndexError as e:
        logger.error(module_name, sys._getframe().f_code.co_name, EnhanceUtills.traceback_printer(), system_id)
    finally:
        return job_name_list


def get_task_executable_job_list(server, sys_id_series):
    """
    the sync method to get task executable job list

    :param server:
    :param sys_id_series:
    :return:
    """
    task_executable_job_list = []

    for i, sys_id in sys_id_series.iteritems():
        executable_job_list = get_prod_deploy_job_name_list_by_sys_id(server, sys_id)
        task_executable_job_list.append(executable_job_list)
    return task_executable_job_list


def get_task_executable_job_list_async(server, task_number, sys_id_series):
    """
    get the executable job list by task number, by invoking async method.

    :param server:
    :param task_number:
    :param sys_id_series: the data series in data frame, which column name is "sys_id"
    :return:
    """

    # sub function to gather asyncio results
    async def async_invoker():
        tasks = [async_get_prod_deploy_job_name_list_by_sys_id(server, sys_id) for i, sys_id in
                 sys_id_series.iteritems()]
        results = await asyncio.gather(*tasks)
        return results

    result_list = asyncio.run(async_invoker())
    logger.info(module_name, sys._getframe().f_code.co_name,
                "the executable job list queried by task id: {} is: {}".format(task_number, result_list)
                , system_id
                )

    return result_list


def build_prod_deploy_job_from_info(server, release_version: str, job_name: str) -> (str, str, int):
    """
    build deploy job by given param on env=prod.

    :param server: jenkins_server
    :param release_version: a release version info. e.g. 'ld171_fims-gateway_No.29339_20200707.1_pre'
    :param job_name: the job name. e.g, 'ld075_bmgs_deply2'
    :return: the jobname, build_number, queue item
    """
    try:
        jenkins_server = server
        job, next_build_number, queue_item = build_deploy_job(jenkins_server, job_name, release_version)
        return job, next_build_number, queue_item
    except Exception as e:
        logger.error(module_name, sys._getframe().f_code.co_name, EnhanceUtills.traceback_printer(),
                     system_id)


def build_add_deploy_record_job(server, release_version: str):
    """
    build the job which add deploy record in zeus db by jenkins.

    :param server:
    :param release_version:
    :return:
    """
    try:
        jenkins_server = server
        job, next_build_number, queue_item = update_deploy_record_job(jenkins_server, release_version)
        return job, next_build_number, queue_item
    except Exception as e:
        logger.error(module_name, sys._getframe().f_code.co_name, EnhanceUtills.traceback_printer(),
                     system_id)


if __name__ == '__main__':
    crowd_client = CrowdClient()
    token = crowd_client.login("guihe", "woshiguihe")
    jenkins_server = connect_server(token, crowd_client)

    # query_build_states?job_name=ld043_online_prod&build_number=272&queue_item=14769
    job_name = "ld048_appgate-web-static_prod"
    build_number = "313"
    queue_item = 14783
    build_number = 40
    queue_item = 17392
    # job_name = "ld229_bigdata-search_prod"
    # param = {'qaVersion': 'ld229_bigdata-search_build_No.29577_20200907.2_T', 'TASK_NO': '29577', 'opser': 'caikai',
    #          'release_memo': 'generated from jenkins CI/CD tool', 'release_exception': 'bug fix'}
    # job_name, build_number, queue_item = build_job_from_info(jenkins_server, param['TASK_NO'], 'ld229',
    #                                                          param['qaVersion'], param['opser'],
    #                                                          param['release_memo'], param['release_exception'])
    result = build_job_by_param(jenkins_server, job_name, param['qaVersion'], param['TASK_NO'], param['opser'],
                                param['release_memo'], param['release_exception'])
    resp = query_build_execution(jenkins_server, job_name, queue_item, build_number)
    print(resp)
    prod_jenkins_server = connect_server(token, crowd_client, env="prod")
    # job_info = prod_jenkins_server.get_job_info("ld075_bmgs_deply2")
    # print(job_info['buildable'])
    # release_version = "ld139_wechatgatemanage_No.18115_20170613.1_pre"
    # result=build_pre_deploy_job_from_info(prod_jenkins_server, release_version)

    # print(result)
    job_list = get_prod_deploy_job_name_list_by_sys_id(prod_jenkins_server, "ld075")
    print(job_list)

    # start = time.time()
    # jobs = prod_jenkins_server.get_all_jobs(folder_depth=0,
    #                                         folder_depth_per_request=10)
    # end = time.time()
    # print(jobs)
    # print("take time:{}".format(end - start))

    start = time.time()
    task_no = 29332
    df_release_version = gittag_extract.get_release_version_to_deploy(task_no, env="prod")
    result = get_task_executable_job_list_async(prod_jenkins_server, task_no, df_release_version['sys_id'])
    # df_release_version["executable_job_list"] = result
    print(result)
    end = time.time()
    print("take time:{}".format(end - start))

    release_version = 'ld183_fims-static_No.29741_20200907.1_prod'
    result = build_add_deploy_record_job(prod_jenkins_server, release_version)

    # print(df_release_version)
    # print(df_git_tag[0:])
    # job_list = []
    # for index, row in df_git_tag[0:].iterrows():
    #     print(index, row['index'], row['gittag'], row['repeat_flag'])
    #     sys_id = row['index']
    #     gittag = row['gittag']
    #     repeat_flag = row['repeat_flag']
    #     memo = 'jenkins tool auto generated'
    #     exception = 'bug fix'
    #     job_name, build_number, queue_item = build_job_from_info(jenkins_server, task_no, sys_id, gittag, 'caikai',
    #                                                              memo, exception)
    #     job_list.append((job_name, build_number, queue_item))
    #     print(job_name, build_number, queue_item)
    #
    # while job_list:
    #     # for loop
    #     to_del = []
    #     for index, value in enumerate(job_list):
    #         job_name, build_number, queue_item = value
    #         resp = query_build_execution(jenkins_server, job_name, queue_item, build_number)
    #         print(resp)
    #         if resp.get("Status") == "Finished":
    #             to_del.append(value)
    #     # after each loop, clean the to_del elements
    #     for item in to_del:
    #         print('{} is removed from list'.format(item))
    #         job_list.remove(item)
    #     # sleep between each loop query
    #     time.sleep(10)

    # build_info = get_build_exec_state(jenkins_server, job_name, build_number)
    # print(build_info['building'], build_info['result'])

    # sys_id = 'ld105'
    # job_name = get_job_by_sysid(jenkins_server, sys_id)
    # build_number = 80
